/*
  author Sylvain Bertrand <digital.ragnarok@gmail.com>
  Protected by GNU Affero GPL v3 with some exceptions.
  See README at root of alga tree.
*/

#include <asm/io.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <asm/unaligned.h>
#include <linux/mutex.h>

#include <alga/amd/atombios/atb.h>

#include "tables/atb.h"
#include "tables/data.h"

#include "atb.h"
#include "interpreter.h"
#include "tables/iio.h"

static u32 read(struct atombios *atb, u16 *ptr)
{
	struct iio_readwrite *iio_read;
	u16 reg_u32_idx;
	u32 val;

	iio_read = atb->adev.rom + *ptr;

	reg_u32_idx = get_unaligned_le16(&iio_read->reg_u32_idx);

	val = atb->adev.rr32(atb->adev.dev, 4 * reg_u32_idx);

	*ptr += sizeof(*iio_read);
	return val;
}

static void write(struct atombios *atb, u16 *ptr, u32 val)
{
	struct iio_readwrite *iio_write;
	u16 reg_u32_idx;

	iio_write = atb->adev.rom + *ptr;

	reg_u32_idx = get_unaligned_le16(&iio_write->reg_u32_idx);

	atb->adev.wr32(atb->adev.dev, val, 4 * reg_u32_idx);

	*ptr += sizeof(*iio_write);
}

static u32 clear(struct atombios *atb, u16 *ptr, u32 val)
{
	struct iio_clear *iio_clear;

	iio_clear = atb->adev.rom + *ptr;

	val &= ~((0xffffffff >> (32 - iio_clear->bits)) << iio_clear->pos);

	*ptr += sizeof(*iio_clear);
	return val;
}

static u32 set(struct atombios *atb, u16 *ptr, u32 val)
{
	struct iio_set *iio_set;

	iio_set = atb->adev.rom + *ptr;
	
	val |= (0xffffffff >> (32 - iio_set->bits)) << iio_set->pos;

	*ptr += sizeof(*iio_set);
	return val;
}

static u32 move_bits(struct atombios *atb, u16 *ptr, u32 dst, u32 src)
{
	struct iio_move_bits *iio_move_bits;

	iio_move_bits = atb->adev.rom + *ptr;

	/* clear bits for the source bits */
	dst &= ~((0xffffffff >> (32 - iio_move_bits->sz))
						<< iio_move_bits->dst_pos);

	/*
	 * get the source bits from their source position and OR them at their
	 * destination position
	 */
	dst |= ((src >> iio_move_bits->src_pos) & (0xffffffff >>
			(32 - iio_move_bits->sz))) << iio_move_bits->dst_pos;

	*ptr += sizeof(*iio_move_bits);
	return dst;
}

u32 iio_interpret(struct ictx *ictx, u16 iio_prog, u32 idx, u32 data)
{
	u32 tmp;
	u16 ptr; 

	tmp = 0xcdcdcdcd;
	ptr = iio_prog;

	while (1)
		switch (*(u8*)(ictx->atb->adev.rom + ptr)) {
		case IIO_NOP:
			ptr++;
			break;
		case IIO_READ:
			tmp = read(ictx->atb, &ptr);
			break;
		case IIO_WRITE:
			write(ictx->atb, &ptr, tmp);
			break;
		case IIO_CLEAR:
			tmp = clear(ictx->atb, &ptr, tmp);
			break;
		case IIO_SET:
			tmp = set(ictx->atb, &ptr, tmp);
			break;
		case IIO_MOVE_IDX:
			tmp = move_bits(ictx->atb, &ptr, tmp, idx);
			break;
		case IIO_MOVE_DATA:
			tmp = move_bits(ictx->atb, &ptr, tmp, data);
			break;
		case IIO_MOVE_ATTR:
			tmp = move_bits(ictx->atb, &ptr, tmp,
						ictx->atb->g_ctx.io_attr);
			break;
		case IIO_END:
			return tmp;
		default:
			dev_err(ictx->atb->adev.dev,
					"atombios: iio: unknown opcode %d\n",
					*(u8*)(ictx->atb->adev.rom + ptr));
			return 0;
		}
}

static int iio_len[] = {
	sizeof(struct iio_nop),
	sizeof(struct iio_start),
	sizeof(struct iio_readwrite),
	sizeof(struct iio_readwrite),
	sizeof(struct iio_clear),
	sizeof(struct iio_set),
	sizeof(struct iio_move_bits),
	sizeof(struct iio_move_bits),
	sizeof(struct iio_move_bits),
	sizeof(struct iio_end)
};

int iio_setup(struct atombios *atb)
{
	u16 offset;
	struct master_data_tbl *data;
	u16 iio;
	struct common_tbl_hdr *iio_tbl_hdr;
	struct iio_start *iio_start;

	/* 256 slots for iio program indexes */
	atb->iio = kzalloc(2 * 256, GFP_KERNEL);
	if (!atb->iio)
		return -ATB_ERR;

	offset = get_unaligned_le16(&atb->hdr->master_data_tbl_offset);
	data = atb->adev.rom + offset;
	iio = get_unaligned_le16(&data->list.iio);
	iio_tbl_hdr = atb->adev.rom + iio;
	dev_info(atb->adev.dev, "atombios: iio (0x%04x) revision %u.%u\n",
		iio, iio_tbl_hdr->tbl_fmt_rev, iio_tbl_hdr->tbl_content_rev);
	iio += sizeof(*iio_tbl_hdr);

	/*
	 * Load the first iio operation index in the proper iio program slot.
	 * See tables/iio.h
	 */
	while (*(u8*)(atb->adev.rom + iio) == IIO_START) {
		iio_start = atb->adev.rom + iio;

		iio += sizeof(*iio_start);
		atb->iio[iio_start->prog_idx] = iio;

		while (*(u8*)(atb->adev.rom + iio) != IIO_END)
			iio += iio_len[*(u8*)(atb->adev.rom + iio)];

		iio += sizeof(struct iio_end);
	}
	return 0;
}
